# -*- coding: utf-8 -*-
import ast
import json
import os
import re
import shlex
import subprocess
import sys
import threading
import configparser
import tkinter as tk
from configparser import ConfigParser
from functools import lru_cache
from pathlib import Path
from tkinter import ttk, filedialog, messagebox, scrolledtext
from typing import Set
from tkinterdnd2 import DND_FILES, TkinterDnD

# ---------- 依赖-参数映射 ----------
COLLECT_RULES = {
	"tkinter": "--collect-all=tkinter",
	"matplotlib": "--collect-all=matplotlib",
	"PyQt5": "--collect-all=PyQt5",
	"PyQt6": "--collect-all=PyQt6",
	"PySide2": "--collect-all=PySide2",
	"PySide6": "--collect-all=PySide6",
	"Pillow": "--collect-all=Pillow",
	"PIL": "--collect-all=Pillow",
	"sklearn": "--collect-all=sklearn",
	"skimage": "--collect-all=skimage",
	"cv2": "--collect-all=cv2",
	"numpy": "--collect-all=numpy",
	"pandas": "--collect-all=pandas",
	"tensorflow": "--collect-all=tensorflow",
	"torch": "--collect-all=torch",
	"xgboost": "--collect-all=xgboost",
	"lightgbm": "--collect-all=lightgbm",
	"selenium": "--collect-all=selenium",
	"pygame": "--collect-all=pygame",
	"reportlab": "--collect-all=reportlab",
	"weasyprint": "--collect-all=weasyprint",
	"requests": "--collect-all=requests",
	"urllib3": "--collect-all=urllib3",
	"bs4": "--collect-all=bs4",
	"lxml": "--collect-all=lxml",
	"flask": "--collect-all=flask",
	"fastapi": "--collect-all=fastapi",
	"sqlalchemy": "--collect-all=sqlalchemy",
	"alembic": "--collect-all=alembic",
	"cryptography": "--collect-all=cryptography",
	"openpyxl": "--collect-all=openpyxl",
	"xlsxwriter": "--collect-all=xlsxwriter",
	"xlrd": "--collect-all=xlrd",
	"pymongo": "--collect-all=pymongo",
	"redis": "--collect-all=redis",
	"psutil": "--collect-all=psutil",
	"yaml": "--collect-all=yaml",
	"jinja2": "--collect-all=jinja2",
	"importlib_metadata": "--collect-all=importlib_metadata",
	"python_dateutil": "--collect-all=python_dateutil",
	"pytz": "--collect-all=pytz",
	"babel": "--collect-all=babel",
	"bcrypt": "--collect-all=bcrypt",
	"nacl": "--collect-all=nacl",
	"soundfile": "--collect-all=soundfile",
	"librosa": "--collect-all=librosa",
	"geopandas": "--collect-all=geopandas",
	"folium": "--collect-all=folium",
	"dash": "--collect-all=dash",
	"streamlit": "--collect-all=streamlit",
	"plotly": "--collect-all=plotly",
	"altair": "--collect-all=altair",
	"webview": "--collect-all=webview",
	"PySimpleGUI": "--collect-all=PySimpleGUI",
	"pkg_resources": "--collect-all=pkg_resources",
	"setuptools": "--collect-all=setuptools",
	"wheel": "--collect-all=wheel",
	"pyparsing": "--hidden-import=pyparsing.util",
	"cffi": "--hidden-import=_cffi_backend",
	"packaging": "--hidden-import=packaging.version",
}

# ---------- 通用工具 ----------
@lru_cache
def resource_path(rel: str) -> str:
	base = Path(sys._MEIPASS) if hasattr(sys, "_MEIPASS") else Path(__file__).parent
	return str(base / rel)

def config_file_path(script_path: str = "") -> str:
	if script_path:
		return str(Path(script_path).resolve().parent / "PyInstaller_Gui.ini")
	return resource_path("PyInstaller_Gui.ini")

def load_cfg(script_path: str = "") -> ConfigParser:
	cfg = ConfigParser()
	cfg.read(config_file_path(script_path), encoding="utf-8")
	if not cfg.has_section("last"):
		cfg.add_section("last")
	if not cfg.has_section("collect"):
		cfg.add_section("collect")
	return cfg

def save_cfg(cfg: ConfigParser, script_path: str = ""):
	try:
		Path(config_file_path(script_path)).write_text(
			"\n".join("[" + k + "]\n" + "\n".join(f"{k2}={v2}" for k2, v2 in sect.items()) for k, sect in cfg.items() if
			          k != "DEFAULT"),
			encoding="utf-8",
		)
	except Exception as e:
		print("保存 ini 失败", e)

@lru_cache
def list_venv_dirs():
	found = {}
	for vname in ("venv", ".venv"):
		vpath = Path(__file__).resolve().parent / vname
		interp = vpath / "Scripts/python.exe"
		if interp.exists():
			found[vname] = str(interp)
	return found


def list_all_interpreters():
	base = {"<系统解释器>": "python"}
	base.update(list_venv_dirs())
	return base




# ---------- 依赖扫描 ----------
def _top_imports(path: Path) -> Set[str]:
	tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
	names = set()
	for node in ast.walk(tree):
		if isinstance(node, ast.Import):
			names.update(alias.name.split(".")[0] for alias in node.names)
		elif isinstance(node, ast.ImportFrom) and node.module:
			names.add(node.module.split(".")[0])
	return names

def _installed(python: str) -> Set[str]:
	try:
		txt = subprocess.check_output([python, "-m", "pip", "list", "--format=freeze"], text=True, timeout=10)
		return {line.split("==")[0].lower() for line in txt.splitlines() if line}
	except Exception:
		return set()

def scan_missing_packages(script_path: Path, python: str) -> Set[str]:
	try:
		imports = _top_imports(script_path)
		have = _installed(python)
		return {pkg for pkg in imports if pkg.lower() not in have}
	except Exception:
		return set()


# ---------- 主界面 ----------
class PyInstallerGUI:
	def __init__(self, root: TkinterDnD.Tk):
		self.root = root
		self.root.title("PyInstaller GUI for Windows 11")
		self.root.geometry("1000x890")
		self.root.minsize(1000, 890)
		self.proc = None
		self.root.protocol("WM_DELETE_WINDOW", self.on_close)
		self.cfg = load_cfg()
		self._build_ui()


	# ---------- UI ----------
	def _build_ui(self):
		# 0. 虚拟环境
		frm = ttk.LabelFrame(self.root, text="0. 虚拟环境")
		frm.pack(fill="x", padx=10, pady=5)
		self.venv_map = list_all_interpreters()
		self.venv_combo = ttk.Combobox(frm, width=75, state="readonly")
		self.venv_combo["values"] = list(self.venv_map.keys())
		self.venv_combo.set(self.cfg.get("last", "venv", fallback="<系统解释器>"))
		self.venv_combo.pack(side="left", padx=5)
		ttk.Button(frm, text="刷新", command=self.refresh_venv).pack(side="left", padx=5)
		ttk.Button(frm, text="创建 .venv", command=self.create_venv).pack(side="left", padx=5)
		ttk.Button(frm, text="分析依赖", command=self.analyze_deps).pack(side="left", padx=5)
		ttk.Button(frm, text="虚拟环境命令行",command=self.open_script_venv_console).pack(side="left", padx=5)


		# 1. 基本设置
		frm = ttk.LabelFrame(self.root, text="1. 基本设置")
		frm.pack(fill="x", padx=10, pady=5)
		self.vars = {}
		for idx, (key, text, filters) in enumerate(
				[("script", "脚本文件", [("Python", "*.py")]), ("icon", "图标文件", [("图标", "*.ico")]),
				 ("outdir", "输出目录", None)]
		):
			self.vars[key] = tk.StringVar(value=self.cfg.get("last", key, fallback=""))
			self.add_path_row(frm, idx, text, self.vars[key], filters)
		
		# 2. 高级选项
		frm2 = ttk.LabelFrame(self.root, text="2. 高级选项")
		frm2.pack(fill="x", padx=10, pady=5)
		self.mode = tk.StringVar(value="onefile" if self.cfg.getboolean("last", "onefile", fallback=True) else "onedir")
		for col, (val, text) in enumerate([("onefile", "单文件 --onefile"), ("onedir", "单目录 --onedir")]):
			ttk.Radiobutton(frm2, text=text, value=val, variable=self.mode).grid(row=0, column=col, sticky="w", padx=5)
		
		opts = [("windowed", "隐藏控制台 (-w)"), ("clean", "打包前清理 (--clean)"), ("upx", "使用 UPX 压缩")]
		self.opts = {k: tk.BooleanVar(value=self.cfg.getboolean("last", k, fallback=(k != "upx"))) for k, _ in opts}
		for col, (k, text) in enumerate(opts, start=2):
			ttk.Checkbutton(frm2, text=text, variable=self.opts[k]).grid(row=0, column=col, sticky="w", padx=5)
		
		upx_frm = ttk.Frame(frm2)
		upx_frm.grid(row=1, column=0, columnspan=6, sticky="ew", padx=5, pady=3)
		ttk.Label(upx_frm, text="UPX 路径").pack(side="left")
		self.upx_path = tk.StringVar(value=self.cfg.get("last", "upx_path", fallback=""))
		ttk.Entry(upx_frm, textvariable=self.upx_path, width=65).pack(side="left", padx=5)
		ttk.Button(upx_frm, text="浏览…",
		           command=lambda: self._on_path_browse(self.upx_path, [("EXE", "upx.exe")])).pack(side="left")
		
		# 2.4 一键收集常用依赖
		frm_collect = ttk.LabelFrame(self.root, text="2.4 一键收集常用依赖")
		frm_collect.pack(fill="both", padx=10, pady=5)
		canvas = tk.Canvas(frm_collect, height=60)
		scrollbar = ttk.Scrollbar(frm_collect, orient="vertical", command=canvas.yview)
		self.collect_frame = ttk.Frame(canvas)
		self.collect_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
		canvas.create_window((0, 0), window=self.collect_frame, anchor="nw")
		canvas.configure(yscrollcommand=scrollbar.set)
		canvas.pack(side="left", fill="both", expand=True)
		scrollbar.pack(side="right", fill="y")
		self.collect_vars = {}
		
		for idx, pkg in enumerate(COLLECT_RULES):
			var = tk.BooleanVar(value=self.cfg.getboolean("collect", pkg, fallback=False))
			self.collect_vars[pkg] = var
			ttk.Checkbutton(self.collect_frame, text=pkg, variable=var).grid(
				row=idx // 10, column=idx % 10, sticky="w", padx=3, pady=2
			)
		# 让鼠标滚轮可以滚动 frm_collect 区域
		canvas.bind_all("<MouseWheel>",
		                lambda e: canvas.yview_scroll(int(-1 * (e.delta / 120)), "units"))
		# 2.5 附加命令
		frm3 = ttk.LabelFrame(self.root, text="2.5 附加命令（一行一个参数）")
		frm3.pack(fill="x", padx=10, pady=5)
		self.extra_cmd_text = scrolledtext.ScrolledText(frm3, height=2, width=90)
		self.extra_cmd_text.pack(fill="both", padx=5, pady=5)
		self.extra_cmd_text.insert("1.0", self.cfg.get("last", "extra_cmd", fallback=""))
		# 2.6 附加文件/目录
		self._build_file_list("2.6 附加文件/目录 (datas)", "datalist", self.add_files_or_dirs, "remove_selected_datas",
		                      "clear_datas")
		# 2.7 附加二进制
		self._build_file_list("2.7 附加二进制 (binaries)", "binlist", self.add_binaries, "remove_selected_binaries",
		                      "clear_binaries")
		# 3. 日志
		frm6 = ttk.LabelFrame(self.root, text="3. 日志")
		frm6.pack(fill="both", expand=True, padx=10, pady=5)
		self.log = scrolledtext.ScrolledText(frm6, height=7, width=85, state="normal")
		self.log.pack(fill="both", padx=5, pady=5)
		self.log.bind_all("<Control-s>", lambda e: self.save_log())
		
		# 4. 按钮
		frm7 = ttk.Frame(self.root)
		frm7.pack(fill="x", padx=10, pady=5)
		self.btn_start = ttk.Button(frm7, text="开始打包", command=self.start_pack)
		self.btn_start.pack(side="left")
		self.btn_cancel = ttk.Button(frm7, text="取消", command=self.cancel_pack, state="disabled")
		self.btn_cancel.pack(side="left", padx=5)
		
		self.status = ttk.Label(self.root, text="就绪")
		self.status.pack(side="bottom", fill="x")
	
	def _build_file_list(self, label, cfg_key, add_cmd, remove_cmd, clear_cmd):
		frm = ttk.LabelFrame(self.root, text=label)
		frm.pack(fill="both", expand=True, padx=10, pady=5)
		btn_bar = ttk.Frame(frm)
		btn_bar.pack(fill="x")
		ttk.Button(btn_bar, text="添加文件", command=lambda: add_cmd("file")).pack(side="left", padx=2)
		ttk.Button(btn_bar, text="添加目录", command=lambda: add_cmd("dir")).pack(side="left", padx=2)
		ttk.Button(btn_bar, text="删除选中", command=getattr(self, remove_cmd)).pack(side="left", padx=2)
		ttk.Button(btn_bar, text="清空", command=getattr(self, clear_cmd)).pack(side="left", padx=2)
		scroll = ttk.Scrollbar(frm, orient="vertical")
		lst = tk.Listbox(frm, selectmode=tk.EXTENDED, height=3, yscrollcommand=scroll.set)
		scroll.config(command=lst.yview)
		scroll.pack(side="right", fill="y")
		lst.pack(fill="both", expand=True, padx=5, pady=5)
		lst.drop_target_register(DND_FILES)
		lst.dnd_bind("<<Drop>>", lambda e: self.on_drop_list(e, lst))
		lst.bind("<Double-1>", self.open_selected_in_explorer)
		setattr(self, "file_list" if "data" in cfg_key else "bin_list", lst)
		for p in self.cfg.get("last", cfg_key, fallback="").split("|"):
			if p.strip():
				lst.insert(tk.END, p.strip())
	
	def add_path_row(self, parent, row, label, var, filters):
		ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", padx=5, pady=2)
		ent = ttk.Entry(parent, textvariable=var, width=65)
		ent.grid(row=row, column=1, padx=5)
		ent.drop_target_register(DND_FILES)
		ent.dnd_bind("<<Drop>>", lambda e: self._on_path_drop(e, var))
		cmd = lambda: self._on_path_browse(var, filters)
		ttk.Button(parent, text="浏览…", command=cmd).grid(row=row, column=2, padx=5)
		if label == "脚本文件":
			ttk.Button(parent, text="加载配置", command=self.load_config).grid(row=row, column=3, padx=2)
			ttk.Button(parent, text="保存配置", command=self.save_config).grid(row=row, column=4, padx=2)
			
	def open_script_venv_console(self):
		script_path = Path(self.vars["script"].get())
		cur = Path(script_path).resolve().parent
		interp = cur/".venv"/"Scripts/activate.bat"
		work_dir =cur/".venv"/"Scripts"
		clean_env = {'SystemRoot': os.environ['SystemRoot']}
		venv_bat = Path(str(interp))
		self.log_insert(f"{interp}\n")
		subprocess.Popen(f'cmd.exe /k "{venv_bat}"',creationflags=subprocess.CREATE_NEW_CONSOLE,cwd=work_dir,env=clean_env,close_fds=True)
		
	# ---------- 其余方法 ----------
	def _on_path_browse(self, var, filters):
		new_path = filedialog.askopenfilename(filetypes=filters) if filters else filedialog.askdirectory()
		if new_path:
			var.set(new_path)
			if var is self.vars["script"]:
				self.try_switch_to_dot_venv(new_path)
	
	def _on_path_drop(self, event, var):
		new_path = shlex.split(re.sub(r"[{}]", "", event.data))[0]
		var.set(new_path)
		if var is self.vars["script"]:
			self.try_switch_to_dot_venv(new_path)
	
	def try_switch_to_dot_venv(self, script_path):
		if not script_path:
			return
		cur = Path(script_path).resolve().parent
		for base in (cur, *cur.parents):
			interp = base / ".venv" / "Scripts/python.exe"
			if interp.exists():
				name = f".venv({base.name})" if base != cur else ".venv"
				self.venv_map[name] = str(interp)
				self.venv_combo["values"] = list(self.venv_map.keys())
				self.venv_combo.set(name)
				break
	
	def refresh_venv(self):
		current = self.venv_combo.get()
		self.venv_map = list_all_interpreters()
		self.venv_combo["values"] = list(self.venv_map.keys())
		if current in self.venv_map:
			self.venv_combo.set(current)
	
	def get_interpreter(self):
		choice = self.venv_combo.get()
		return self.venv_map.get(choice, sys.executable)
	
	def add_files_or_dirs(self, mode):
		lst = self.file_list
		if mode == "file":
			files = filedialog.askopenfilenames()
			for f in files:
				lst.insert(tk.END, f)
		else:
			d = filedialog.askdirectory()
			if d:
				lst.insert(tk.END, d)
	
	def remove_selected_datas(self):
		for idx in reversed(self.file_list.curselection()):
			self.file_list.delete(idx)
	
	def clear_datas(self):
		self.file_list.delete(0, tk.END)
	
	def add_binaries(self, mode):
		files = filedialog.askopenfilenames(filetypes=[("DLL", "*.dll"), ("All", "*.*")])
		for f in files:
			self.bin_list.insert(tk.END, f)
	
	def remove_selected_binaries(self):
		for idx in reversed(self.bin_list.curselection()):
			self.bin_list.delete(idx)
	
	def clear_binaries(self):
		self.bin_list.delete(0, tk.END)
	
	def on_drop_list(self, event, lst):
		paths = shlex.split(re.sub(r"[{}]", "", event.data))
		for p in paths:
			lst.insert(tk.END, p)
	
	def open_selected_in_explorer(self, _):
		for lst in (self.file_list, self.bin_list):
			if lst.curselection():
				path = Path(lst.get(lst.curselection()[0]))
				if path.exists():
					os.startfile(path if path.is_dir() else path.parent)
	
	# ---------- 配置 ----------
	def load_config(self):
		script = self.vars["script"].get()
		if not script:
			messagebox.showwarning("提示", "请先选择脚本文件！")
			return
		self.cfg = load_cfg(script)
		self.apply_cfg_to_ui()
		messagebox.showinfo("提示", "配置已加载！")
	
	def save_config(self):
		script = self.vars["script"].get()
		if not script:
			messagebox.showwarning("提示", "请先选择脚本文件！")
			return
		self.save_ui_to_cfg(script)
		messagebox.showinfo("提示", "配置已保存！")
	
	def apply_cfg_to_ui(self):
		self.venv_combo.set(self.cfg.get("last", "venv", fallback="<系统解释器>"))
		for k in ("script", "icon", "outdir"):
			self.vars[k].set(self.cfg.get("last", k, fallback=""))
		self.mode.set("onefile" if self.cfg.getboolean("last", "onefile", fallback=True) else "onedir")
		for k in ("windowed", "clean", "upx"):
			self.opts[k].set(self.cfg.getboolean("last", k, fallback=(k != "upx")))
		self.upx_path.set(self.cfg.get("last", "upx_path", fallback=""))
		self.extra_cmd_text.delete("1.0", "end")
		self.extra_cmd_text.insert("1.0", self.cfg.get("last", "extra_cmd", fallback=""))
		
		for lst_key, lst in [("datalist", self.file_list), ("binlist", self.bin_list)]:
			lst.delete(0, tk.END)
			for p in self.cfg.get("last", lst_key, fallback="").split("|"):
				if p.strip():
					lst.insert(tk.END, p.strip())
		
		for pkg, var in self.collect_vars.items():
			var.set(self.cfg.getboolean("collect", pkg, fallback=False))
	
	def save_ui_to_cfg(self, script_path: str = ""):
		cfg = {
			"last": {
				"venv": self.venv_combo.get(),
				"script": self.vars["script"].get(),
				"icon": self.vars["icon"].get(),
				"outdir": self.vars["outdir"].get(),
				"extra_cmd": self.extra_cmd_text.get("1.0", "end-1c").strip(),
				"datalist": "|".join(self.file_list.get(0, tk.END)),
				"binlist": "|".join(self.bin_list.get(0, tk.END)),
				"onefile": str(self.mode.get() == "onefile"),
				"onedir": str(self.mode.get() == "onedir"),
				"windowed": str(self.opts["windowed"].get()),
				"clean": str(self.opts["clean"].get()),
				"upx": str(self.opts["upx"].get()),
				"upx_path": self.upx_path.get(),
			},
			"collect": {pkg: str(var.get()) for pkg, var in self.collect_vars.items()},
		}
		self.cfg.read_dict(cfg)
		save_cfg(self.cfg, script_path or self.vars["script"].get())
	
	# ---------- 依赖处理 ----------
	def export_requirements(self):
		script_path = Path(self.vars["script"].get())
		if not script_path.is_file():
			messagebox.showwarning("提示", "请先选择脚本文件！")
			return
		target = script_path.with_name("requirements.txt")
		interpreter = self.get_interpreter()
		self.log_insert("\n【开始生成 requirements.txt】\n")
		try:
			txt = subprocess.check_output([interpreter, "-m", "pip", "freeze"], text=True, timeout=30)
			target.write_text(txt, encoding="utf-8")
			self.log_insert(f"已保存 → {target}\n{txt}")
		except Exception as e:
			self.log_insert(f"导出失败：{e}\n")
	
	def analyze_deps(self):
		script = Path(self.vars["script"].get())
		if not script.is_file():
			messagebox.showwarning("提示", "请先选择脚本文件！")
			return
		self.export_requirements()
		interpreter = self.get_interpreter()
		self.log_insert("\n【开始分析依赖】\n")
		missing = scan_missing_packages(script, interpreter)
		if missing:
			self.log_insert(f"缺失的包：{', '.join(missing)}\n")
			threading.Thread(target=self._install_missing, args=(missing, interpreter), daemon=True).start()
		else:
			self.log_insert("无缺失包。\n")
	
	def _install_missing(self, missing, interpreter):
		self.btn_start.config(state="disabled")
		stdlib_pkgs = {
			'__future__', '_abc', '_aix_support', '_ast', '_asyncio', '_bisect', '_blake2', '_bootsubprocess', '_bz2',
			'_codecs', '_codecs_cn', '_codecs_hk', '_codecs_iso2022', '_codecs_jp', '_codecs_kr', '_codecs_tw',
			'_collections', '_collections_abc', '_compat_pickle', '_compression', '_contextvars', '_crypt', '_csv',
			'_ctypes', '_curses', '_curses_panel', '_datetime', '_dbm', '_decimal', '_elementtree', '_frozen_importlib',
			'_frozen_importlib_external', '_functools', '_gdbm', '_hashlib', '_heapq', '_imp', '_io', '_json',
			'_locale', '_lsprof', '_lzma', '_markupbase', '_md5', '_msi', '_multibytecodec', '_multiprocessing',
			'_opcode', '_operator', '_osx_support', '_overlapped', '_pickle', '_posixshmem', '_posixsubprocess',
			'_py_abc', '_pydecimal', '_pyio', '_queue', '_random', '_scproxy', '_sha1', '_sha256', '_sha3', '_sha512',
			'_signal', '_sitebuiltins', '_socket', '_sqlite3', '_sre', '_ssl', '_stat', '_statistics', '_string',
			'_strptime', '_struct', '_symtable', '_thread', '_threading_local', '_tkinter', '_tokenize', '_tracemalloc',
			'_typing', '_uuid', '_warnings', '_weakref', '_weakrefset', '_winapi', '_zoneinfo', 'abc', 'aifc',
			'antigravity', 'argparse', 'array', 'ast', 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'base64',
			'bdb', 'binascii', 'bisect', 'builtins', 'bz2', 'cProfile', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmath',
			'cmd', 'code', 'codecs', 'codeop', 'collections', 'colorsys', 'compileall', 'concurrent', 'configparser',
			'contextlib', 'contextvars', 'copy', 'copyreg', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses',
			'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'distutils', 'doctest', 'email', 'encodings', 'ensurepip',
			'enum', 'errno', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'fractions', 'ftplib',
			'functools', 'gc', 'genericpath', 'getopt', 'getpass', 'gettext', 'glob', 'graphlib', 'grp', 'gzip',
			'hashlib', 'heapq', 'hmac', 'html', 'http', 'idlelib', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect',
			'io', 'ipaddress', 'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma',
			'mailbox', 'mailcap', 'marshal', 'math', 'mimetypes', 'mmap', 'modulefinder', 'msilib', 'msvcrt',
			'multiprocessing', 'netrc', 'nis', 'nntplib', 'nt', 'ntpath', 'nturl2path', 'numbers', 'opcode', 'operator',
			'optparse', 'os', 'ossaudiodev', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform',
			'plistlib', 'poplib', 'posix', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile',
			'pyclbr', 'pydoc', 'pydoc_data', 'pyexpat', 'queue', 'quopri', 'random', 're', 'readline', 'reprlib',
			'resource', 'rlcompleter', 'runpy', 'sched', 'secrets', 'select', 'selectors', 'shelve', 'shlex', 'shutil',
			'signal', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'spwd', 'sqlite3', 'sre_compile',
			'sre_constants', 'sre_parse', 'ssl', 'stat', 'statistics', 'string', 'stringprep', 'struct', 'subprocess',
			'sunau', 'symtable', 'sys', 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile',
			'termios', 'textwrap', 'this', 'threading', 'time', 'timeit', 'tkinter', 'token', 'tokenize', 'tomllib',
			'trace', 'traceback', 'tracemalloc', 'tty', 'turtle', 'turtledemo', 'types', 'typing', 'unicodedata',
			'unittest', 'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref', 'webbrowser', 'winreg',
			'winsound', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib', 'zoneinfo'
		}
		to_install = ["Pyinstaller"]
		for pkg in missing:
			# 跳过标准库和已安装的包
			if pkg not in stdlib_pkgs and pkg not in sys.modules:
				to_install.append(pkg)
		cmd = [interpreter, "-m", "pip", "install"] + to_install
		self.root.after(0, lambda: self.log_insert("执行安装：\n" + " ".join(cmd) + "\n"))
		try:
			proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8")
			for line in proc.stdout:
				self.root.after(0, lambda l=line: self.log_insert(l))
			proc.wait()
			self.root.after(0, lambda: self.log_insert("\n依赖安装完成\n" if proc.returncode == 0 else "\n安装失败\n"))
			self.root.after(0, lambda: self.btn_start.config(state="normal"))
		finally:
			self.root.after(0, lambda: self.btn_start.config(state="normal"))
	
	# ---------- 打包 ----------
	def start_pack(self):
		script = Path(self.vars["script"].get())
		if not script.is_file():
			messagebox.showwarning("提示", "请先选择脚本文件！")
			return
		try:
			compile(script.read_text(encoding="utf-8"), script, "exec")
		except SyntaxError as e:
			messagebox.showerror("语法错误", str(e))
			return
		self.btn_start.config(state="disabled")
		self.btn_cancel.config(state="normal")
		self.log.delete("1.0", "end")
		threading.Thread(target=self.run_pyinstaller, daemon=True).start()
	
	def cancel_pack(self):
		if self.proc and self.proc.poll() is None:
			self.proc.terminate()
			self.log_insert("\n已取消打包\n")
		self.btn_cancel.config(state="disabled")
		self.btn_start.config(state="normal")
	
	def log_insert(self, text):
		self.log.configure(state="normal")
		self.log.insert("end", text)
		self.log.configure(state="disabled")
		self.log.see("end")
	
	def run_pyinstaller(self):
		script = Path(self.vars["script"].get())
		cmd = [self.get_interpreter(), "-m", "PyInstaller", str(script)]
		if self.mode.get() == "onefile":
			cmd += ["--onefile", "--runtime-tmpdir", "."]
		else:
			cmd += ["--onedir"]
		
		cmd += [f"--{k}" for k in ("windowed", "clean") if self.opts[k].get()]
		if icon := self.vars["icon"].get():
			cmd += ["--icon", icon]
		script_dir = script.parent
		cmd += ["--specpath", str(script_dir), "--workpath", str(script_dir / "build")]
		if outdir := self.vars["outdir"].get():
			cmd += ["--distpath", outdir]
			Path(outdir).mkdir(parents=True, exist_ok=True)
		if self.opts["upx"].get():
			cmd += ["--upx-dir", self.upx_path.get() or "."]
		
		extra_lines = [line.strip() for line in self.extra_cmd_text.get("1.0", "end-1c").splitlines() if line.strip()]
		for arg in extra_lines:
			cmd.extend(shlex.split(arg))
		
		for f in self.file_list.get(0, tk.END):
			dst = "." if Path(f).is_file() else Path(f).name
			cmd += [f"--add-data={f}{os.pathsep}{dst}"]
		for f in self.bin_list.get(0, tk.END):
			cmd += [f"--add-binary={f}{os.pathsep}."]
		
		# 2.4 一键收集
		for pkg, var in self.collect_vars.items():
			if var.get():
				cmd.extend(shlex.split(COLLECT_RULES[pkg]))
		
		self.save_ui_to_cfg()
		self.log_insert("【当前执行命令】\n" + " ".join(cmd) + "\n\n")
		self.status.config(text="打包中…")
		try:
			self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8",
			                             bufsize=1)
			for line in self.proc.stdout:
				self.root.after(0, lambda l=line: self.log_insert(l))
			self.proc.wait()
			self.root.after(0, lambda: self.log_insert(
				"\n打包完成！\n" if self.proc.returncode == 0 else "\n打包失败，请查看日志\n"))
			self.root.after(0, lambda: messagebox.showinfo("完成",
			                                               "打包成功！" if self.proc.returncode == 0 else "打包失败，请查看日志"))
		finally:
			self.root.after(0, lambda: self.btn_start.config(state="normal"))
			self.root.after(0, lambda: self.btn_cancel.config(state="disabled"))
			self.root.after(0, lambda: self.status.config(text="就绪"))
	
	def save_log(self):
		log_file = Path.home() / "pyinstaller.log"
		log_file.write_text(self.log.get("1.0", "end-1c"), encoding="utf-8")
		messagebox.showinfo("日志已保存", str(log_file))
	
	def on_close(self):
		if self.proc and self.proc.poll() is None:
			self.proc.kill()
		self.root.destroy()
		


	# ---------- 虚拟环境 ----------
	def create_venv(self):
		script = Path(self.vars["script"].get())
		if not script.is_file():
			messagebox.showwarning("提示", "请先选择脚本！")
			return
		venv_path = script.parent / ".venv"
		if venv_path.exists():
			messagebox.showinfo("提示", "已存在 .venv")
			return
		self.log_insert("\n【开始创建 .venv】\n")
		self.btn_start.config(state="disabled")
		threading.Thread(target=self._do_create_venv, args=(venv_path,), daemon=True).start()
	
	def _do_create_venv(self, venv_path):
		cmd = ["python ", "-m", "venv", str(venv_path)]
		proc = subprocess.run(cmd, capture_output=True, text=True)
		self.root.after(0, lambda: self.log_insert(proc.stdout))
		self.root.after(0, lambda: self.log_insert(
			"\n【虚拟环境创建成功】\n" if proc.returncode == 0 else "\n创建失败\n" + proc.stderr))
		self.root.after(0, self.refresh_venv)
		self.try_switch_to_dot_venv(venv_path)
		self.root.after(0, lambda: self.btn_start.config(state="normal"))

# ---------- 入口 ----------
if __name__ == "__main__":
	root = TkinterDnD.Tk()
	PyInstallerGUI(root)
	root.mainloop()
